define(['AABB'], function (AABB) {

//================================================================================
//region Basic Models and Utilities
//================================================================================

    function LngLat(lng, lat) {
        if (lng && lat) {
            this.lng = lng;
            this.lat = lat;
        } else if (lng && lng[0] && lng[1]) {
            this.lng = lng[0];
            this.lat = lat[0];
        } else {
            this.lng = 0;
            this.lat = 0;
        }
    }

    var pi = 3.1415926535897932384626;
    var a = 6378245.0;
    var ee = 0.00669342162296594323;

    function outOfChina(lng, lat) {
        if (lng < 72.004 || lng > 137.8347)
            return true;
        if (lat < 0.8293 || lat > 55.8271)
            return true;
        return false;
    }

    function transform(lng, lat) {
        if (outOfChina(lng, lat)) {
            return new LngLat(lng, lat);
        }
        var dLat = transformLat(lng - 105.0, lat - 35.0);
        var dLon = transformLon(lng - 105.0, lat - 35.0);
        var radLat = lat / 180.0 * pi;
        var magic = Math.sin(radLat);
        magic = 1 - ee * magic * magic;
        var sqrtMagic = Math.sqrt(magic);
        dLat = (dLat * 180.0) / ((a * (1 - ee)) / (magic * sqrtMagic) * pi);
        dLon = (dLon * 180.0) / (a / sqrtMagic * Math.cos(radLat) * pi);
        var mgLat = lat + dLat;
        var mgLon = lng + dLon;
        return new LngLat(mgLon, mgLat);
    }

//dlng, dlat
    function transformLat(x, y) {
        var ret = -100.0 + 2.0 * x + 3.0 * y + 0.2 * y * y + 0.1 * x * y
            + 0.2 * Math.sqrt(Math.abs(x));
        ret += (20.0 * Math.sin(6.0 * x * pi) + 20.0 * Math.sin(2.0 * x * pi)) * 2.0 / 3.0;
        ret += (20.0 * Math.sin(y * pi) + 40.0 * Math.sin(y / 3.0 * pi)) * 2.0 / 3.0;
        ret += (160.0 * Math.sin(y / 12.0 * pi) + 320 * Math.sin(y * pi / 30.0)) * 2.0 / 3.0;
        return ret;
    }

//dlng, dlat
    function transformLon(x, y) {
        var ret = 300.0 + x + 2.0 * y + 0.1 * x * x + 0.1 * x * y + 0.1
            * Math.sqrt(Math.abs(x));
        ret += (20.0 * Math.sin(6.0 * x * pi) + 20.0 * Math.sin(2.0 * x * pi)) * 2.0 / 3.0;
        ret += (20.0 * Math.sin(x * pi) + 40.0 * Math.sin(x / 3.0 * pi)) * 2.0 / 3.0;
        ret += (150.0 * Math.sin(x / 12.0 * pi) + 300.0 * Math.sin(x / 30.0
            * pi)) * 2.0 / 3.0;
        return ret;
    }

    function wgs84_to_gcj02(lng, lat) {
        return transform(lng, lat);
    }

    /*
     * Since WGS84 to GCJ02 transformation is monotonic, we can use a
     * minimizing rectangle box containing the target point to recursively
     * approach the target point.
     *
     * We know the max deviation of WGS84 to GCJ02 is around 700 meters
     * so it's safe to use a initial box of 0.015 degree radius (max 0.0102)
     * https://kongxz.com/2013/10/wgs-cgj/
     */
    function gcj02_to_wgs84(gcjlng, gcjlat, precision) {
        /*
         * 1e1: 111km
         * 1e-1: 11.1km
         * 1e-2: 1.11km
         * 1e-3: 111m
         * 1e-4: 11m
         * 1e-5: 1m
         * 1e-6: 0.1m
         * 1e-7: 1cm
         * 1e-8: 1mm
         */
        precision = precision || 1e-6;

        if (gcjlng.lng && gcjlng.lat) {
            gcjlat = gcjlng.lat;
            gcjlng = gcjlng.lng;
        }

        var wgsRect = new AABB();
        var delta = 0.015;
        wgsRect.combineXY(gcjlng + delta, gcjlat + delta);
        wgsRect.combineXY(gcjlng - delta, gcjlat - delta);

        /*
         * in case it doesn't converge (theorically impossible), breaks it
         * regularly, 15 iters (1e-15) can reach the best precision of a 64bit float
         * number (javascript)
         */
        var maxIter = 20;

        while ((maxIter--) > 0) {
            var cenX = wgsRect._cenX || wgsRect.cenX;
            var cenY = wgsRect._cenY || wgsRect.cenY;
            var cen_gcj = wgs84_to_gcj02(cenX, cenY);

            /*
             * the point should be in the box, else errors happen, the nearest point
             * in the box will be returned
             */

            if (Math.abs(cen_gcj.lng - gcjlng) > precision || Math.abs(cen_gcj.lat - gcjlat) > precision) {
                if (cen_gcj.lng > gcjlng) {
                    if (cen_gcj.lat > gcjlat) {
                        // bottom left
                        wgsRect.maxX = cenX;
                        wgsRect.maxY = cenY;
                    } else {
                        // top left
                        wgsRect.maxX = cenX;
                        wgsRect.minY = cenY;
                    }
                } else {
                    if (cen_gcj.lat > gcjlat) {
                        // bottom right
                        wgsRect.minX = cenX;
                        wgsRect.maxY = cenY;
                    } else {
                        // top right
                        wgsRect.minX = cenX;
                        wgsRect.minY = cenY;
                    }
                }
                wgsRect._cenX = (wgsRect.minX + wgsRect.maxX) / 2;
                wgsRect._cenY = (wgsRect.minY + wgsRect.maxY) / 2;
            } else {
                break;
            }
        }
        return new LngLat(cenX, cenY);
    }

// window.gcj02_to_wgs84 = gcj02_to_wgs84;
// window.wgs84_to_gcj02 = wgs84_to_gcj02;

//================================================================================
//endregion
//================================================================================

    return {
        gcj02_to_wgs84: gcj02_to_wgs84,
        wgs84_to_gcj02: wgs84_to_gcj02,
    };

});